<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta charset="utf-8">
    <meta name="apple-mobile-web-app-title" content="宝宝名字大全">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="renderer" content="webkit">
    <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
    <title>PHP 原生websocket实现聊天室功能</title>
    <meta name="keywords" content="php websocket,websocket,php实现websocket,原生php实现websocket"/>
    <meta name="description" content="php单文件开发websocket服务例子"/>
    <link rel="shortcut icon" href="//ikam.cn/view/img/favicon.ico"/>
    <link rel="icon" sizes="32x32" href="//ikam.cn/view/img/favicon.ico">
    <link rel="Bookmark" href="//ikam.cn/view/img/favicon.ico"/>
    <link rel="stylesheet" href="//ikam.cn/view/css/bootstrap.min.css">
    <link rel="stylesheet" href="//ikam.cn/view/css/bootstrap-bbs.min.css">
    <link rel="stylesheet" href="//ikam.cn/view/htm/bing/style.css">
    <style>
        #header {
            z-index: 5;
            opacity: 0.6;
            height: 3%;
            display: flex;
            justify-content: center;
            align-items: center;
            text-align: center;
        }

        .main {
            z-index: 5;
            width: 100%;
            height: 88%;
            display: table-cell;
            vertical-align: middle;
            text-align: center;
        }

        .box, .title {
            z-index: 5;
            opacity: 0.6;
            text-align: left;
            border-radius: 10px;
            margin: 0.5% auto;
        }

        .title {
            height: 10%;
            display: flex;
            justify-content: center; /* 水平居中 */
            align-items: center; /* 垂直居中 */
            text-align: center; /* 文字水平居中 */
        }

        .box {
            overflow: auto;
            height: 88%;
        }

        #footer {
            z-index: 5;
            position: absolute;
            bottom: 0;
            opacity: 0.6;
            height: 7%;
        }
        
        .loading {
            text-align: center;
            color: #999;
            padding: 10px;
            display: none;
        }
    </style>
</head>
<body>
<header class="bg-light" id="header">
    <div class="container">
        <span id="notice">大众聊天室[仅作演示用]</span>
    </div>
</header>
<div id="background" class="wall"></div>
<div id="midground" class="wall"></div>
<div id="foreground" class="wall"></div>
<div class="main">
    <div class="container bg-light box" id="msg">
        <div class="loading" id="loading">加载更多消息...</div>
        <div id="historyMessages"></div>
    </div>
    <h4 class="container bg-light title" style="font-size: 120%">
        <div class="input-group mb-3">
            <label for="txt" class="container">我是[<span class="name" id="nickname" style="color: red"></span>]</label>
            <input type="text" class="form-control" placeholder="消息内容" id="txt">
            <div class="input-group-append">
                <button class="btn btn-primary" id="send">发送</button>
            </div>
        </div>
    </h4>
    <p id="text">文明发言 健康聊天</p>
    <p id="author" style="z-index:96;">By:可爱猫-小黄</p>
</div>
<script src="//ikam.cn/lang/zh-cn/bbs.js"></script>
<script src="//ikam.cn/view/js/jquery-3.5.1.min.js"></script>
<script src="//ikam.cn/view/js/popper.min.js"></script>
<script src="//ikam.cn/view/js/bootstrap.min.js"></script>
<script src="//ikam.cn/view/js/xiuno.min.js"></script>
<script src="//ikam.cn/view/js/bootstrap-plugin.min.js"></script>
<script src="//ikam.cn/view/js/async.min.js"></script>
<script src="//ikam.cn/view/js/form.js"></script>
<script>
    /**
     0：未连接
     1：连接成功，可通讯
     2：正在关闭
     3：连接已关闭或无法打开
     */
    let messages = $('#msg');
    let historyMessages = $('#historyMessages');
    let loading = $('#loading');
    let isLoading = false;
    let currentStart = 0;
    let hasMoreMessages = true;
    
    //let url = 'wss://www.ikam.cn/websocket';
    let url = 'ws://127.0.0.1:3233';
    let webSocket = new WebSocket(url);

    // 辅助函数：转义正则表达式中的特殊字符
    function escapeRegExp(string) {
        return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    }
    
    // 替换消息中的昵称为"我"
    function replaceNicknameWithMe(message, nickname) {
        if (typeof message !== 'string') {
            console.log("消息不是字符串:", message);
            return message;
        }
        
        console.log("开始替换昵称. 原始消息:", message);
        console.log("当前用户昵称:", nickname);
        
        // 首先检查消息是否有可能是格式不正确的HTML（span标签没有正确闭合）
        if (message.includes('data-nickname') && !message.includes('</span>')) {
            console.log("检测到HTML格式可能不正确，尝试修复");
            
            // 尝试修复格式不正确的HTML
            message = message.replace(/(data-nickname=['"][^'"]*['"][^>]*>)([^<]*)([\]：])/g, 
                function(match, attributes, content, end) {
                    return attributes + content + '</span>' + end;
                }
            );
            console.log("修复后的消息:", message);
        }
        
        // 在使用DOM解析之前先检查消息是否包含了当前用户昵称
        if (!message.includes(nickname)) {
            console.log("消息中不包含当前用户昵称，无需替换");
            return message;
        }
        
        // 检查消息格式，确保结构完整性
        const messageFormat = /\[<span[^>]*>[^<]*<\/span>\]：/;
        if (message.match(messageFormat)) {
            console.log("消息格式符合预期: [<span>内容</span>]：文本");
        } else {
            console.log("警告: 消息格式可能不符合预期");
        }
        
        // 如果直接包含data-nickname属性，使用DOM解析处理
        if (message.includes('data-nickname')) {
            try {
                // 使用正则表达式匹配出前后结构，确保替换不会丢失部分
                const regex = /(\[)<span[^>]*data-nickname=['"]([^'"]*)['"][^>]*>([^<]*)<\/span>(\])：(.*)/i;
                const match = message.match(regex);
                
                if (match) {
                    const [full, openBracket, dataNickname, content, closeBracket, restOfMessage] = match;
                    console.log("匹配分析:", {openBracket, dataNickname, content, closeBracket, restOfMessage});
                    
                    if (dataNickname.toLowerCase() === nickname.toLowerCase()) {
                        console.log("昵称匹配，替换为'我'");
                        const replaced = `${openBracket}<span class="name" data-nickname="${dataNickname}" style="color: purple">我</span>${closeBracket}：${restOfMessage}`;
                        console.log("替换后结果:", replaced);
                        return replaced;
                    }
                } else {
                    console.log("无法使用正则表达式匹配完整消息结构");
                }
                
                // 如果正则方法失败，回退到DOM操作
                const tempDiv = document.createElement('div');
                tempDiv.innerHTML = message;
                
                // 查找所有带有data-nickname属性的span元素
                const spans = tempDiv.querySelectorAll('span[data-nickname]');
                let modified = false;
                
                spans.forEach(span => {
                    const spanNickname = span.getAttribute('data-nickname');
                    console.log("找到带data-nickname的span:", spanNickname);
                    
                    // 如果昵称匹配当前用户
                    if (spanNickname && spanNickname.toLowerCase() === nickname.toLowerCase()) {
                        console.log("找到匹配的昵称，将替换为'我'");
                        span.textContent = "我";
                        span.setAttribute('style', 'color: purple');
                        modified = true;
                    }
                });
                
                if (modified) {
                    // 确保我们没有丢失消息的任何部分
                    const result = tempDiv.innerHTML;
                    console.log("DOM替换结果:", result);
                    
                    // 检查是否缺少了"]"
                    if (message.includes(']：') && !result.includes(']：')) {
                        console.log("检测到缺少']'，手动添加回来");
                        const fixedResult = result.replace(/(<\/span>)：/g, '$1]：');
                        console.log("修复后结果:", fixedResult);
                        return fixedResult;
                    }
                    
                    return result;
                }
            } catch (error) {
                console.error("DOM解析替换昵称时出错:", error);
            }
        }
        
        // 如果DOM解析失败或没有data-nickname属性，使用正则表达式替换
        const nicknamePattern = escapeRegExp(nickname);
        
        // 使用精确的正则表达式模式匹配，保留]符号
        const pattern = new RegExp(`(\\[<span[^>]*>)\\s*${nicknamePattern}\\s*(</span>\\])(：.*)`, 'gi');
        let replaced = message.replace(pattern, '$1我$2$3');
        
        console.log("使用正则替换后的消息:", replaced);
        return replaced;
    }
    
    // 加载全部历史消息
    function loadAllHistoryMessages() {
        // 直接请求2048条消息(系统最大保存量)
        const historyRequest = JSON.stringify({
            event: 'history',
            start: 0,
            count: 2048,
            nickname: infoArr.nickname
        });
        sendWebSocketMessage(historyRequest);
    }
    
    // 加载历史消息
    function loadHistoryMessages(start, count) {
        if (isLoading) {
            console.log("已有加载请求正在进行中，跳过本次请求");
            return;
        }
        
        if (!hasMoreMessages && start > 0) {
            console.log("没有更多历史消息可加载");
            return;
        }
        
        console.log("发送加载历史消息请求:", {start, count});
        isLoading = true;
        loading.show();
        
        const historyRequest = JSON.stringify({
            event: 'history',
            start: start,
            count: count,
            nickname: infoArr.nickname
        });
        sendWebSocketMessage(historyRequest);
        
        // 设置请求超时处理
        setTimeout(function() {
            if (isLoading) {
                console.log("历史消息请求超时");
                isLoading = false;
                loading.hide();
            }
        }, 5000);
    }
    
    //监听消息
    webSocket.onmessage = function (event) {
        console.log("onMessage|Msg:", event.data);
        
        try {
            const data = JSON.parse(event.data);
            console.log("解析JSON数据:", data);
            
            // 处理服务器返回的错误消息
            if (data.event === 'error') {
                console.error("服务器错误:", data.message, "错误代码:", data.code);
                
                // 根据错误类型执行不同的处理
                if (data.action === 'resend' && lastSentMessage) {
                    console.log("先发送join事件确保已加入聊天室");
                    // 先发送join消息，确保用户已加入聊天室
                    const joinMessage = JSON.stringify({
                        event: "join",
                        nickname: infoArr.nickname
                    });
                    
                    webSocket.send(joinMessage);
                    
                    // 延迟500ms后重发之前的消息
                    console.log("然后尝试重新发送上一条消息:", lastSentMessage);
                    setTimeout(function() {
                        webSocket.send(lastSentMessage);
                    }, 500); // 延迟500ms重发
                    return;
                }
                
                // 显示错误消息给用户
                historyMessages.append(`<p style="color:red">[系统提示] ${data.message}</p>`);
                messages.scrollTop(messages[0].scrollHeight);
                return;
            }
            
            // 处理历史消息响应
            if (data.event === 'history') {
                isLoading = false;
                loading.hide();
                
                console.log("历史消息数据:", data);
                console.log("当前开始位置:", currentStart);
                console.log("总消息数:", data.total);
                
                if (data.messages.length === 0) {
                    // 如果没有消息返回，说明可能已经加载完所有消息
                    hasMoreMessages = (currentStart < data.total);
                    console.log("没有更多消息可加载, hasMoreMessages =", hasMoreMessages);
                    return;
                }
                
                const currentNickname = infoArr.nickname;
                console.log("当前用户昵称:", currentNickname);
                
                // 将消息添加到顶部（注意需要反转顺序，因为Redis中是新消息在前）
                let html = '';
                for (let i = data.messages.length - 1; i >= 0; i--) {
                    let message = data.messages[i];
                    console.log("处理历史消息:", message);
                    
                    // 解析消息格式 "昵称:内容"
                    const formattedMessage = formatMessage(message, currentNickname);
                    html += "<p>" + formattedMessage + "</p>";
                }
                
                // 记住当前滚动位置
                const oldHeight = messages[0].scrollHeight;
                const oldScrollTop = messages.scrollTop();
                
                // 在顶部插入历史消息
                historyMessages.prepend(html);
                
                // 计算新增内容高度，并调整滚动位置
                const newHeight = messages[0].scrollHeight;
                messages.scrollTop(oldScrollTop + (newHeight - oldHeight));
                
                // 更新当前起始位置
                currentStart += data.messages.length;
                
                // 更新是否还有更多消息
                hasMoreMessages = (currentStart < data.total);
                console.log("更新后 currentStart =", currentStart, "hasMoreMessages =", hasMoreMessages);
                
                return;
            }
        } catch (e) {
            // 不是JSON格式，按普通消息处理
            console.info("非Json消息", event.data, e);
        }
        
        // 普通消息处理 - 直接显示
        historyMessages.append("<p>" + event.data + "</p>");
        messages.scrollTop(messages[0].scrollHeight);
    };
    
    // 用于存储最后发送的消息，以便在出错时重发
    var lastSentMessage = '';
    
    // 发送消息前记录内容
    function sendWebSocketMessage(message) {
        if (webSocket.readyState === 1) { // OPEN
            lastSentMessage = message;
            webSocket.send(message);
        } else {
            historyMessages.append("<p style='color:red'>[系统提示] WebSocket连接已断开，请刷新页面重试</p>");
            messages.scrollTop(messages[0].scrollHeight);
        }
    }
    
    // 格式化消息，根据昵称判断颜色和显示
    function formatMessage(message, currentNickname) {
        console.log("格式化消息:", message);
        
        // 检查是否是简单格式的消息 "昵称:内容"
        const colonIndex = message.indexOf(':');
        if (colonIndex === -1) {
            console.log("消息格式不是 '昵称:内容'，原样返回");
            return message;
        }
        
        // 提取昵称和内容
        const nickname = message.substring(0, colonIndex).trim();
        const content = message.substring(colonIndex + 1).trim();
        console.log("提取的昵称:", nickname, "内容:", content);
        
        // 判断是否是当前用户
        const isCurrentUser = nickname.toLowerCase() === currentNickname.toLowerCase();
        console.log("是否是当前用户:", isCurrentUser);
        
        // 根据用户判断显示样式
        if (isCurrentUser) {
            return `[<span class="name" style="color: purple">我</span>]：${content}`;
        } else {
            return `[<span class="name" style="color: blue">${nickname}</span>]：${content}`;
        }
    }
    
    // 监听错误
    webSocket.onerror = function (event) {
        historyMessages.append("<p>onError|state:" + sockState() + "</p>");
        console.log("error" + event.data);
        webSocket.close();
    };
    // 监听连接关闭
    webSocket.onclose = function (event) {
        console.log("onClose|state:" + sockState());
        setTimeout(reconnect, 200);
    };
    // 打开websocket
    webSocket.onopen = function (event) {
        console.log(webSocket);
        console.log("onOpen|state:" + sockState());
        historyMessages.append("<p>onOpen|state:" + sockState() + "</p>");
        
        // 加入聊天
        sendWebSocketMessage(JSON.stringify({
            event: 'join',
            nickname: infoArr.nickname,
        }));
        
        // 加载所有历史消息(而不是仅5条)
        console.log("首次连接 - 加载全部历史消息");
        loadAllHistoryMessages();
        
        messages.scrollTop(messages[0].scrollHeight);
    };
    
    // 确保在DOM完全加载后绑定事件
    $(document).ready(function() {
        // 由于首次加载全部消息，移除加载更多按钮
        console.log("文档就绪 - 已直接加载全部历史消息");
        
        // 保留滚动事件仅用于调试
        messages.off('scroll');
        messages.on('scroll', function() {
            // 获取更详细的滚动信息
            const scrollTop = messages.scrollTop();
            const scrollHeight = messages[0].scrollHeight;
            const clientHeight = messages[0].clientHeight;
            const scrollPercentage = (scrollTop / (scrollHeight - clientHeight)) * 100;
            
            console.log("滚动事件:", {
                scrollTop,
                scrollHeight,
                clientHeight,
                scrollPercentage: scrollPercentage.toFixed(2) + "%"
            });
        });
    });

    // websocket 断线重连
    function reconnect() {
        historyMessages.append("<p><span style='color: red'>重连中...</span></p>");
        messages.scrollTop(messages[0].scrollHeight);
        webSocket.close();
        
        let ws = new WebSocket(url);
        
        // 保存当前的事件处理器
        const oldOnMessage = webSocket.onmessage;
        const oldOnError = webSocket.onerror;
        const oldOnClose = webSocket.onclose;
        
        // 自定义连接成功后的处理
        ws.onopen = function(event) {
            console.log("重连成功，重新加入聊天室");
            historyMessages.append("<p><span style='color: green'>已重新连接</span></p>");
            
            // 重新发送join事件
            const joinMessage = JSON.stringify({
                event: "join",
                nickname: infoArr.nickname
            });
            ws.send(joinMessage);
            
            // 如果有最后发送的消息且不是join事件，则重发
            if (lastSentMessage && !lastSentMessage.includes('"event":"join"')) {
                console.log("重发最后一条消息:", lastSentMessage);
                setTimeout(function() {
                    ws.send(lastSentMessage);
                }, 500);
            }
            
            messages.scrollTop(messages[0].scrollHeight);
        };
        
        // 设置其他事件处理器
        ws.onmessage = oldOnMessage;
        ws.onerror = oldOnError;
        ws.onclose = oldOnClose;
        
        // 更新WebSocket对象
        webSocket = ws;
    }

    // websocket 心跳
    const intervalId = setInterval(() => {
        console.log('ws heart ...');
        sendWebSocketMessage(JSON.stringify({
            event: 'ping'
        }));
    }, 55000);//55秒一次，据我观察1分钟不操作会掉线
    //clearInterval(intervalId);

    function sockState() {
        let status = ['连接中', '已连接', '关闭中', '已关闭'];
        return status[webSocket.readyState];
    }

    // 修改发送按钮事件使用新的发送函数
    $('#send').click(function () {
        let msg = $('#txt').val();
        document.getElementById('txt').value = '';
        if (msg === '') {
            historyMessages.append("<p>[<span style='color: rebeccapurple'>系统中心</span>]：<span style='color: red'><span class='name'>" + infoArr.nickname + "</span>,您好!消息内容不能为空哦！</span></p>");
            messages.scrollTop(messages[0].scrollHeight);
            return;
        }
        console.log("start|sendMsg:" + msg);
        
        // 使用新的发送函数
        sendWebSocketMessage(JSON.stringify({
            event: 'send',
            nickname: infoArr.nickname,
            msg: msg,
        }));
    });
    $('#txt').on('keypress', function (event) {
        if (event.keyCode === 13) {
            $('#send').click();
        }
    });

    let info = localStorage.getItem("info");
    let infoArr = {};
    if (info) {
        infoArr = JSON.parse(info);
    } else {
        let nickname1 = ['欧阳', '太史', '端木', '上官', '司马', '东方', '独孤', '南宫', '万俟', '闻人', '夏侯', '诸葛', '尉迟', '公羊', '赫连', '澹台', '皇甫', '宗政', '濮阳', '公冶', '太叔', '申屠', '公孙', '慕容', '仲孙', '钟离', '长孙', '宇文', '司徒', '鲜于', '司空', '闾丘', '子车', '亓官', '司寇', '巫马', '公西', '颛孙', '壤驷', '公良', '漆雕', '乐正', '宰父', '谷梁', '拓跋', '夹谷', '轩辕', '令狐', '段干', '百里', '呼延', '东郭', '南门', '羊舌', '微生', '公户', '公玉', '公仪', '梁丘', '公仲', '公上', '公门', '公山', '公坚', '左丘', '公伯', '西门', '公祖', '第五', '公乘', '贯丘', '公皙', '南荣', '东里', '东宫', '仲长', '子书', '子桑', '即墨', '达奚', '褚师'];
        let nickname2 = ['轩辕', '醉月', '浮光', '缥缈', '星雨', '秋颜', '半夏', '暖阳', '涟漪', '锦瑟', '韶华', '惊鸿', '缠绵', '霓裳', '嬛绵', '山岚', '明秀', '春华', '波澜', '润玉', '紫萱', '羽裳', '翩飞', '落霞', '醉梦', '缱绻', '归云', '忘忧', '瑞雪', '梵音', '烟雨', '蝶舞', '鸿鹄', '青衣', '子衿', '轩辕', '醉月', '浮光', '缥缈', '星雨', '秋颜', '半夏', '暖阳', '涟漪', '锦瑟', '韶华', '惊鸿', '缠绵', '霓裳', '嬛绵', '山岚', '明秀', '春华', '波澜', '润玉', '紫萱', '羽裳', '翩飞', '落霞', '醉梦', '缱绻', '归云', '忘忧', '瑞雪', '梵音', '烟雨', '蝶舞', '鸿鹄', '青衣', '子衿', '妖娆'];
        let nickname = nickname1[Math.floor(Math.random() * nickname1.length)] + nickname2[Math.floor(Math.random() * nickname2.length)];
        infoArr = {nickname: nickname}
        localStorage.setItem("info", JSON.stringify(infoArr))
    }
    $('#nickname').text(infoArr.nickname);

    $('.name').click(function () {
        let name = $(this).text();
        name = name.replace("【", "");
        name = name.replace("】", "");
        window.location.href = "https://www.wiera.cn/index/cm.html?from=ergedd&name=" + escape(name);
    });
    $('.sur').click(function () {
        let sur = $(this).text();
        window.location.href = "https://www.wiera.cn/index/qm.html?from=ergedd&surname=" + escape(sur);
    });
</script>
<footer class="text-muted bg-light small py-1 mt-3" id="footer">
    <div class="container">
        <div class="row">
            <div class="col">
                版权所有，保留一切权利。<br/>
                <a href="https://beian.miit.gov.cn/" target="_blank">京ICP备13050724号-4</a>
            </div>
            <div class="col text-right">
                <b>赐子千金，不如教子一艺；教子一艺，不如赐子一名。</b>
            </div>
        </div>
    </div>
</footer>
</body>
</html>